NULL equ 0x00
SETCHAR equ 0x07
VIDEOMEM equ 0xb800
STRINGLEN equ 0xffff

section head align=16 vstart=0
    Size dd ProgramEnd                      ;4B 0x00，整个程序的长度，包含头部分
    SegmentAddr:
    CodeSeg dd section.code.start           ;4B 0x04, nasm 提供的 '.' 语法，表示 code 段地址
    DataSeg dd section.data.start           ;4B 0x08
    StackSeg dd section.stack.start         ;4B 0x0C
    SegmentNum:
    SegNum db (SegmentNum-SegmentAddr)/4    ;1B 0x10，段的数量，由于每个段的地址固定占 4B，所以除以 4 就能知道具体有多少个段了
    Entry dw CodeStart                      ;2B 0x11，偏移地址
          dd section.code.start             ;4B 0x13，段地址

section code align=16 vstart=0
;声明程序入口点
CodeStart:
    mov ax, [DataSeg]   ;初始化 ds 和 si，指向字符串的开头
    mov ds, ax
    xor si, si
    call PrintString    ;调用打印方法
    jmp $

;声明 PrintString 函数
;用 [ds:si] 指向要显示的字符串的地址
;用 [es:di] 指向显存中的地址
PrintString:
    .setup:             ;设置 setup 标号，紧接着的代码是做一些初始化的工作
    push ax
    push bx
    push cx
    push dx
    mov ax, VIDEOMEM
    mov es, ax          ;将 ds 设置为显示字符的起始位置，用 si 作为偏移地址
    xor di, di

    mov bh, SETCHAR     ;显示字符的属性都为白底黑字
    mov cx, STRINGLEN   ;字符串长度最长为 0xffff，所以最多循环那么多次

    .printchar:         ;设置 printchar 标号，表示紧接着的代码是打印字符
    mov bl, [ds:si]     ;将要显示的字符存到 bl 中
    inc si              ;将 di + 1，指向下一个待显示的字符
    mov [es:di], bl     ;将 bl 中存储的字符设置到显存指定地址中
    inc di              ;为了防止覆盖，需要将显存地址 + 1
    mov [es:di], bh     ;设置该字符的属性，即白底黑字
    inc di
    or bl, NULL         ;可以通过标志寄存器判断当前是不是读到了字符串的结尾
    jz .return          ;如果已经读到了字符串的结尾，则跳转到退出的标号处
    loop .printchar     ;继续打印下一个字符

    .return:
    mov bx, di
    pop dx
    pop cx
    pop bx
    pop ax
    ret

section data align=16 vstart=0
    Hello db 'Hello,I com from program on sector 1,loaded by bootloader!'
section stack align=16 vstart=0
    resb 128    ;reserve byte，保留字节，即先占下一定空间，空间的大小就是后面的数字
                ;不会进行初始化，最开始是啥就是啥

section end align=16
    ProgramEnd: ;这个标号主要作用就是计算整个程序的长度